#include <assert.h>
#include <iostream>
#include <iomanip>
#include <string>
#include <sstream>
#include <fstream>
#include <ctime>
#include <boost/program_options.hpp>
#include <gl/glew.h>
#include <gl/glut.h>
#include <cg/cgGL.h>
#include "kernel.h"
#include "ErrorCheck.h"



void callbackCgError()
{
	CGerror lastError = cgGetError();
	assert(lastError == CG_NO_ERROR);
}



namespace po = boost::program_options;
int main(int argc, char **argv)
{
	std::stringstream ss;
	std::string shaderFile;
	int errorno, width, height, nData;
	GLuint fb;
	float eps, *bodies, *accelsGPU, *accelsCPU;
	double tickGPU, tickCPU;
	clock_t start, stop;

	// init glut
	glutInit(&argc, argv);
	glutCreateWindow("no window");

	// init parameters
	po::options_description opt("Options");
	opt.add_options()
		("est_error",
		"Estimate error value")
		("shader_obj",
		po::value<std::string>()->default_value("shader.og"),
		"Object file name of the shader")
		("simple",
		"Simple output")
		("gpu_dump",
		po::value<std::string>(),
		"Dump results on gpu")
		("cpu_dump",
		po::value<std::string>(),
		"Dump results on cpu")
		("epsilon",
		po::value<float>()->default_value(1.0e-6f),
		"Minimum distance of bodies")
		("width",
		po::value<unsigned>()->default_value(128),
		"Width of the texture")
		("height",
		po::value<unsigned>()->default_value(1),
		"Height of the texture")
		("show_help",
		"Show this help message");
	po::variables_map vm;
	po::store(parse_command_line(argc, argv, opt), vm);
	po::notify(vm);

	// set parameters
	if (vm.count("show_help")) {
		std::cout << opt << std::endl;
		return 0;
	}
	eps = vm["epsilon"].as<float>();
	width = vm["width"].as<unsigned>();
	//nSeries = vm["series"].as<unsigned>();
	height = vm["height"].as<unsigned>();
	nData = width * height * 4;
	bodies = new float[nData];
	accelsGPU = new float[nData];
	accelsCPU = new float[nData];
	shaderFile = vm["shader_obj"].as<std::string>();

	// init glew
	errorno = glewInit();
	assert(errorno == GLEW_OK);

	// init frame buffer object
	glGenFramebuffersEXT(1, &fb);
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);

	// map 1:1 texture/pixel
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluOrtho2D(0, width, 0, height);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glViewport(0, 0, width, height);

	// init cg
	cgSetErrorCallback(callbackCgError);

	// init bodies
	plasterBodies(bodies, nData, 1000, 1000);
	//printVec4(bodies, nData);

	// calc on GPU
	Kernel kernel(width, height, rect_nv_rgba_32, shaderFile.c_str());
	start = clock();
	kernel.calc(bodies, accelsGPU, eps);
	stop = clock();
	tickGPU = (double)(stop - start) / CLOCKS_PER_SEC;

	// check for errors
	assert(glGetError() == GL_NO_ERROR);
	assert(glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT) == GL_FRAMEBUFFER_COMPLETE_EXT);

	// log out
	if (vm.count("gpu_dump")) {
		std::string logName = vm["gpu_dump"].as<std::string>();
		std::ofstream logFile(logName.c_str());
		assert(logFile.is_open());
		printVec4(accelsGPU, nData, logFile);
	}


	// calc on CPU and estimate error
	if (!vm.count("est_error"))
		return 0;

	start = clock();
	computeOnCPU(bodies, accelsCPU, eps, nData);
	stop = clock();
	tickCPU = (double)(stop - start) / CLOCKS_PER_SEC;
	
	if (vm.count("simple")) {
		float dMax, dAvg;
		estimateError(accelsGPU, accelsCPU, nData, dMax, dAvg);
		std::cout << std::scientific << std::setprecision(2)
			<< nData / 4 << "\t"
			<< tickGPU << "\t" 
			<< tickCPU << "\t"
			<< dMax << "\t"
			<< dAvg << std::endl;
	} else {
		std::cout << "N-body simulation: N=" << nData / 4 << std::endl;
		std::cout << std::scientific << std::setprecision(2)
		<< "Elapsed time (sec): gpu=" << tickGPU << ", cpu=" << tickCPU
		<< std::endl;
		describeError(accelsGPU, accelsCPU, nData);
	}

	// log out
	if (vm.count("cpu_dump")) {
		std::string logName = vm["cpu_dump"].as<std::string>();
		std::ofstream logFile(logName.c_str());
		assert(logFile.is_open());
		printVec4(accelsCPU, nData, logFile);
	}

	return 0;
}
